package org.showminifier.web;

import java.util.regex.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;

import org.showminifier.web.RequestHTTPHeader;



/**
*
* Show Minifier
* Author: Shihua Ma <beijing.josh@gmail.com>
* Copyright (c) 2010 Shihua Ma
* Licensed under the MIT license.
*
*/

public class UrlParser {
	
	private class WrapInputStream extends InputStream{
		
		private InputStream in;
		private int length = 0;
		public WrapInputStream(InputStream in){
			this.in = in;
		}
		
		@Override
		public int read() throws IOException {
			int l = in.read();
			length += l;
			return l;
		}
		
		public int read(byte[] b) throws IOException{
			return read(b,0,b.length);
		}
		
		public int read(byte[] b,int start,int len) throws IOException{
			int l = in.read(b, start, len);
			length += l;
			return l;
		}
		
		public int available() throws IOException {
			return in.available();
		}
		
		public long skip(long n) throws IOException {
			length += n;
			return in.skip(n);
		}
		
		public void close() throws IOException {
			in.close();
		}
		
		public synchronized void mark(int readlimit) {
			in.mark(readlimit);
		}
		
		public synchronized void reset() throws IOException {
			in.reset();
		}
		
		public boolean markSupported() {
			return in.markSupported();
		}
		
		public int length(){
			return length;
		}
	}
	
	//The rfc-2612 default value;
	private static String DEFAULT_CHARSET = "ISO-8859-1";
	private static String DEFAULT_TYPE = "application/octetstream";
	
	private static final int CHUNK_SIZE = 2000;
	private static Pattern metaPattern =
	    Pattern.compile("<meta\\s+([^>]*http-equiv=\"?content-type\"?[^>]*)>",
	                    Pattern.CASE_INSENSITIVE);
	  private static Pattern charsetPattern =
	    Pattern.compile("charset=\\s*([a-z][_\\-0-9a-z]*)",
	                    Pattern.CASE_INSENSITIVE);
	  
	private HttpURLConnection connection;
	private ArrayList<ResponseHTTPHeader> responseHeaders;
	private WrapInputStream in = null;
	private String charset;
	private String type;
	private boolean deflated = false;
	private int deflatedLength;
	private int contentLength;
	private String content = null;
	private boolean finished;
	private Date start;
	private URL url;
	private boolean isDafaultCharset = false;
	
	private static final Logger log = Logger.getLogger(UrlParser.class.getName());
	
	public UrlParser(URL url){
		this(url,null);	
	}
	
	public UrlParser(URL url, List<RequestHTTPHeader> headers){
		this.url = url;
		start = new Date();
		try {
			connection = (HttpURLConnection) url.openConnection();
		} catch (IOException e) {
			log.severe("Could not initialize connection at url:" + url);
			throw new IllegalArgumentException();
		}
		if(headers != null && headers.size() > 0){
			setRequestProperty(headers);
		}
		this.responseHeaders = new ArrayList<ResponseHTTPHeader>();
	}
	
	public void setRequestProperty(List<RequestHTTPHeader> headers) {
		Iterator<RequestHTTPHeader> it = headers.iterator();
		while(it.hasNext()){
			RequestHTTPHeader header = it.next();
			String name = header.getName();
			if(name.equalsIgnoreCase("user-agent")){
				connection.setRequestProperty(name,header.getValue() + " ShowMinifier/0.1");
				//force Google Frontend not forward Accept-Encoding;
				if(header.getValue().indexOf("gzip") != -1){
					connection.setRequestProperty("Accept-Encoding","gzip,deflate");
				}
			}else{
				connection.setRequestProperty(name,header.getValue());
			}
		}
        connection.setUseCaches(false);
	}
	
	public int getResponseCode() throws IOException{
		return connection.getResponseCode();
	}
	
	private WrapInputStream getInputStream() throws IOException{
		if(in == null){
			in = new WrapInputStream(connection.getInputStream());
		}
		return in;
	}
	
	public ArrayList<ResponseHTTPHeader> getResponseHeader() throws IOException{
		if(getResponseCode() != HttpURLConnection.HTTP_OK){
			return null;
		};
		if(!responseHeaders.isEmpty()){
			return responseHeaders;
		}
		Iterator it =connection.getHeaderFields().keySet().iterator();
        while(it.hasNext()){
        	String key = (String)it.next();
        	if(key != null)
        		responseHeaders.add(new ResponseHTTPHeader(Util.prettyName(key,false),connection.getHeaderField(key)));
        }
        String t = connection.getContentType();
        if(t == null){
        	this.type = DEFAULT_TYPE;
        	this.charset = DEFAULT_CHARSET;
        	isDafaultCharset = true;
        }
        int start = t.indexOf(';');
		if(start != -1){
			this.type = t.substring(0, start);
			start = t.indexOf("charset=", start);
			if(start != -1){
				this.charset = t.substring(start + 8);
			}
		}else{
			this.type = t;
        	this.charset = DEFAULT_CHARSET;
        	isDafaultCharset = true;
		}
        return responseHeaders;
	}
	
	public boolean isChunked(){
		String encoding = connection.getHeaderField("transfer-encoding");
		return encoding != null && encoding.equalsIgnoreCase("chuncked");
	}
	
	public boolean isDeflated(){
		return deflated;
	}
	
	public int contentLength(){
		return contentLength;
	}
	
	public int defaltedLength(){
		return deflatedLength;
	}
	
	public String getCharset(){
		return charset;
	}
	
	public String getContent(){
		return content;
	}
	
	public String getContentEncoding(){
		return  connection.getContentEncoding();
	}
	
	public String getContentType(){
		return type;
	}
	
	public void parser() throws IOException{		
		if(finished){
			return;
		}
		start = new Date();
		finished = true;
		getResponseHeader();
		if(getResponseCode() != HttpURLConnection.HTTP_OK){
			return;
		};
		WrapInputStream in = getInputStream();
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		if(getContentType().equalsIgnoreCase("text/html") || "application/xhtml+xml".equalsIgnoreCase(getContentType())){
			if(getContentEncoding() == null){				
				int l = -1;
		        byte[] t = new byte[4096];
		        while ((l = in.read(t)) != -1) {
		        	out.write(t, 0, l);
		        }
		        charset = isDafaultCharset ? sniffCharacterEncoding(out.toByteArray()) : getCharset();
		        content=out.toString(charset);
		        contentLength = content.length();
		        in.close();
		        out.close();
		        deflatedLength = Util.defaltedLength(content,getCharset());
			}else if(getContentEncoding().equalsIgnoreCase("gzip") || "x-gzip".equalsIgnoreCase(getContentEncoding())){
				deflated = true;
		        GZIPInputStream gzipIn = new GZIPInputStream(in);
		        int l = -1;
		        byte[] t = new byte[4096];
		        while ((l = gzipIn.read(t)) != -1) {
		        	out.write(t, 0, l);
		        } 
		        deflatedLength = in.length();
		        charset = isDafaultCharset ? sniffCharacterEncoding(out.toByteArray()) : getCharset();
		        content=out.toString(charset);
		        contentLength = content.length();
			}else{
				//lzw compress
			}
			log.info("Parser url["+ url +"]" +" time[" + ( new Date().getTime() - start.getTime() )+ "]");
		}else{
			log.info("Another resource type \"" + getContentType() + "\" request by url[" + url + "]");
			throw new ParserException("Another resource type \"" + getContentType() + "\" request by url[" + url + "]");
		}
		
       
	}
	 private static String sniffCharacterEncoding(byte[] content) {
		    int length = content.length < CHUNK_SIZE ? 
		                 content.length : CHUNK_SIZE;

		    // We don't care about non-ASCII parts so that it's sufficient
		    // to just inflate each byte to a 16-bit value by padding. 
		    // For instance, the sequence {0x41, 0x82, 0xb7} will be turned into 
		    // {U+0041, U+0082, U+00B7}. 
		    String str = "";
		    try {
		      str = new String(content, 0, length,
		                       Charset.forName("ASCII").toString());
		    } catch (UnsupportedEncodingException e) {
		      // code should never come here, but just in case... 
		      return DEFAULT_CHARSET;
		    }
		    //System.out.println(str);
		    Matcher metaMatcher = metaPattern.matcher(str);
		    String encoding = null;
		    if (metaMatcher.find()) {
		      Matcher charsetMatcher = charsetPattern.matcher(metaMatcher.group(1));
		      if (charsetMatcher.find()) 
		        encoding = new String(charsetMatcher.group(1));
		    }

		    return encoding == null ? DEFAULT_CHARSET : encoding;
	}
	
}
